import util from 'node:util'
import http from 'node:http'
import Stream from 'node:stream'
import Emitter from 'node:events'
import statuses from '../http/statuses'
import onFinished from '../http/onFinished'
import compose from '../common/compose'
import HttpContext from './HttpContext'
import { IHttpApplication, IHttpApplicationOptions, IHttpContext, IHttpMiddleware } from "../types/index.d.ts"

// https://github.com/koajs/koa/blob/master/lib/application.js
export class HttpApplication extends Emitter implements IHttpApplication {

  silent: boolean
  // Trust proxy headers
  proxy: boolean
  // Subdomain offset
  subdomainOffset: number
  // Proxy IP header, defaults to X-Forwarded-For
  proxyIpHeader: string
  // Max IPs read from proxy IP header, default to 0 (means infinity)
  maxIpsCount: number
  // Environment
  env: string
  // Signed cookie keys
  keys: Array<string>
  middleware: Array<IHttpMiddleware>

  constructor(options: IHttpApplicationOptions = {
    silent: false,
    proxy: false,
    // Subdomain offset
    subdomainOffset: 2,
    // Proxy IP header, defaults to X-Forwarded-For
    proxyIpHeader: 'X-Forwarded-For',
    // Max IPs read from proxy IP header, default to 0 (means infinity)
    maxIpsCount: 0,
    // Environment
    env: 'dev',
    // Signed cookie keys
    keys: []
  }) {
    super()
    this.silent = options.silent || false
    this.proxy = options.proxy || false
    this.subdomainOffset = options.subdomainOffset || 2
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'
    this.maxIpsCount = options.maxIpsCount || 0
    this.env = options.env || process.env.NODE_ENV || 'development'
    if (options.keys) this.keys = options.keys
    // 中间件
    this.middleware = []
  }

  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */
  listen(...args) {
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }

  /**
   * Return JSON representation.
   * We only bother showing settings.
   *
   * @return {Object}
   * @api public
   */
  toJSON() {
    return {
      'subdomainOffset': this.subdomainOffset,
      'proxy': this.proxy,
      'env': this.env
    }
  }

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */
  inspect() {
    return this.toJSON()
  }

  /**
   * Use the given middleware `fn`.
   */
  use(middleware: IHttpMiddleware) {
    if (typeof middleware !== 'function') {
      throw new TypeError('middleware must be a function!')
    }
    // 将中间件塞入到middleware数组
    this.middleware.push(middleware)
    return this
  }

  /**
   * Return a request handler callback for node's native http server.
   */
  callback() {
    // compose来自koa-compose库，就是将中间件合并成一个函数
    const fn = compose(this.middleware)

    if (!this.listenerCount('error')) this.on('error', this.onerror)

    // callback返回值必须符合http.createServer参数形式：(req, res) => {}
    const handleRequest = (req: http.IncomingMessage, res: http.ServerResponse) => {
      const ctx = new HttpContext(this, req, res)
      return this.handleRequest(ctx, fn)
    }

    return handleRequest
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */
  handleRequest(ctx: IHttpContext, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    // 调用中间件处理，所有处理完后就调用handleResponse返回请求
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  }

  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */
  onerror(err) {
    // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
    // See https://github.com/koajs/koa/issues/1466
    // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error
    if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err))

    if (err.status === 404 || err.expose) return
    if (this.silent) return

    const msg = err.stack || err.toString()
    console.error(`\n${msg.replace(/^/gm, '  ')}\n`)
  }
}

/**
 * Response helper.
 */
function respond(ctx) {
  if (!ctx.writable) return

  const res = ctx.res
  let body = ctx.body
  const code = ctx.status

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null
    return res.end()
  }

  if (ctx.method === 'HEAD') {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response
      if (Number.isInteger(length)) ctx.length = length
    }
    return res.end()
  }

  // status body
  if (body == null) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type')
      ctx.response.remove('Transfer-Encoding')
      ctx.length = 0
      return res.end()
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code)
    } else {
      body = ctx.message || String(code)
    }
    if (!res.headersSent) {
      ctx.type = 'text'
      ctx.length = Buffer.byteLength(body)
    }
    return res.end(body)
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body)
  if (typeof body === 'string') return res.end(body)
  if (body instanceof Stream) return body.pipe(res)

  // body: json
  body = JSON.stringify(body)
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body)
  }
  res.end(body)
}
